using System.Windows;
using System.Linq.Expressions;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Linq.Mapping;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; // for FxCop warning suppression
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Transactions;
using System.Xml.Serialization;
using PDO.DataPersistence.ServiceProvider;
using PDO.Utilities;

namespace PDO.DataPersistence.Core
{

    [DataContract]
    public abstract class PersistableDataObject : IPersistableData, INotifyPropertyChanged,
        IDisposable, IDataErrorInfo,
        IEditableObject
    {
        #region Constants

        // Property name of database object's Primary Key column
        public const string PrimaryKeyPropertyName = "PKeyID";

        #endregion

        #region Static

        public static event EventHandler LoadComplete;

        private static List<Assembly> _pdoAssemblies;

        public static void RegisterAssembly(string filePath)
        {
            Assembly assembly = System.Reflection.Assembly.LoadFrom(filePath);
            PdoAssemblies.Add(assembly);
        }

        public static List<Assembly> PdoAssemblies
        {
            get
            {
                if (null == _pdoAssemblies)
                {
                    _pdoAssemblies = new List<Assembly>();
                }
                return PersistableDataObject._pdoAssemblies;
            }
            set { PersistableDataObject._pdoAssemblies = value; }
        }

        #endregion

        #region Private Members

        private bool _loadingFromDataSource; // set true to disable read-only checking when loading from database
        private bool _isImmutable;
        private bool _isValid = true;
        private string _validationStatus;

        private DataSession _session;
        private bool _isNew;
        private bool _isDirty;
        private bool _markedForDeletion;
        private bool _isEthereal;

        private PersistableDataObject _parent;
        private PersistableDataObject _rootParent;

        private bool _changeTrackingEnabeled;

        private int _nestingLevel = -1;
        private bool _isSelfReference;
        private bool? _autoExpand;

        private bool _hasEditChanges;

        private object _syncRoot;

        // Containing list object (if this object is indeed a member of a list)
        private IPersistableDataObjectList _listContainer;

        private char _auditAction;

        Dictionary<MemberInfo, string> _validationErrors;
        string _errorSummary;

        #endregion

        #region Public Events

        // Special purpose event handlers used for key value updates
        public delegate void PrimaryKeyUpdate(object sender);

        [SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")]
        public event PrimaryKeyUpdate PrimaryKeyUpdateHandler;

        internal delegate void ForeignKeyUpdate(object sender);




        private ForeignKeyUpdate _foreignKeyUpdateDelegate;
        private event ForeignKeyUpdate _foreignKeyUpdate;

        // Custom event accessor used to ensure there is only a single parent for this foreign key relationship.
        // When Twinning it could otherwise be possible to get into a scenario with multiple handlers.
        internal event ForeignKeyUpdate ForeignKeyUpdateHandler
        {
            add
            {
                if (null != _foreignKeyUpdateDelegate)
                {
                    _foreignKeyUpdate -= _foreignKeyUpdateDelegate;
                }
                _foreignKeyUpdate += value;
                _foreignKeyUpdateDelegate = value;
            }
            remove { _foreignKeyUpdate -= value; }
        }

        internal void RaiseForeignKeyUpdateEvent(object pkedyID)
        {
            if (null != _foreignKeyUpdate)
            {
                _foreignKeyUpdate(pkedyID);
            }
        }




        // Post- load from database event
        public event EventHandler SubscriptionUpdater;

        public event EventHandler Initializing;
        // Used to "chain" validation events up from child to parent PDOs for PDO associations 
        // built on top of generated classes.  This is particulary useful for propogating validation 
        // information from chile PDOs up to PMs.
        public delegate void ValidationChangedEventHandler(object sender, ValidationEventArgs e);
        public event ValidationChangedEventHandler ValidationChanged;

        #endregion

        #region Constructors and Creation

        static PersistableDataObject()
        {
            //Equivalent must be called at hosting application startup.

            //// TEMPORARY - will be removed when fully database mediated PDO extension refactor is complete.
            //FileSystemItem item = new FileSystemItem("Northwind.DataModel.dll");
            //RegisterAssembly(item.AbsolutePath);

        }

        /// <summary>
        /// Default constructor 
        /// </summary>
        protected PersistableDataObject()
        {
            SyncRoot = new object();
            IsNew = true;
            ChangeTrackingEnabled = true;
        }

        static public T Create<T>(DataSession session)
                where T : PersistableDataObject, new()
        {
            T item = new T();
            item.Session = session;
            return item;
        }

        static public T Create<T>(PersistableDataObject parent)
            where T : PersistableDataObject, new()
        {
            return Create<T>(parent, null);
        }


        static public T Create<T>(PersistableDataObject parent, string extendedClassName)
            where T : PersistableDataObject, new()
        {
            T item = null;

            if (!string.IsNullOrEmpty(extendedClassName))
            {
                Type extendedType = ExtendedTypes.GetTypeByName(extendedClassName);
                if (extendedType != default(Type))
                {
                    item = Activator.CreateInstance(extendedType) as T;
                }
                else
                {
                    throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_EXCLASS_NOTFOUND", "Unable to find extension class '{0}'", extendedClassName));
                }
            }
            else
            {
                item = new T();
                item.Parent = parent;
                if (null != parent)
                {
                    item.Session = parent.Session;
                }
            }
            return item;
        }


        static public T Create<T>(DataSession session, PersistableDataObject parent)
            where T : PersistableDataObject, new()
        {
            T item = new T();
            item.Session = session;
            item.Parent = parent;
            return item;
        }

        static public T Create<T>(PersistableDataObject parent, object pkeyID)
            where T : PersistableDataObject, new()
        {
            return Create<T>(DataSession.DefaultSession, parent, pkeyID);
        }



        static public T Create<T>(DataSession session, PersistableDataObject parent, object pkeyID)
            where T : PersistableDataObject, new()
        {
            string key = PDOMapper.GetSourceDatabaseTableName(typeof(T)) + ":" + pkeyID.ToString();

            T item = Create<T>(session, parent);
            item.Load(pkeyID);

            return GetExtendedItem(item);
        }

        /// <summary>
        /// Create a new PersistableDataObject of specified type.  
        /// Note that underlying type may actually be an ExtendedType derived from specified base type.
        /// </summary>
        /// <typeparam name="T">Type of object to return</typeparam>
        /// <param name="session">DataSession</param>
        /// <param name="parent">parent PersistableDataObject</param>
        /// <param name="dr">DataRow from which to create specified Type</param>
        /// <returns>PersistableDataObject of type T</returns>
        static internal T Create<T>(DataSession session, PersistableDataObject parent, DataRow dr)
            where T : PersistableDataObject, new()
        {
            T item = Create<T>(session, parent);
            item.LoadFromDataRow(dr);

            return GetExtendedItem(item);
        }


        /// <summary>
        /// Create a PersistableDataObject from a provided class name.
        /// </summary>
        /// <param name="parent">parent PersistableDataObject </param>
        /// <param name="className">class name of object to create</param>
        /// <returns>PersistableDataObject object</returns>
        static public PersistableDataObject Create(PersistableDataObject parent, string className)
        {
            return Create(parent, className, null);
        }


        /// <summary>
        /// Create a PersistableDataObject from a provided class name and primary key.
        /// </summary>
        /// <param name="parent">parent PersistableDataObject </param>
        /// <param name="className">class name of object to create</param>
        /// <param name="pkeyID">primary key of base object</param>
        /// <returns>Extended PersistableDataObject object</returns>
        static public PersistableDataObject Create(PersistableDataObject parent, string className, object pkeyID)
        {
            PersistableDataObject pdo = null;
            try
            {
                Type t = PersistableDataObjectPluginManager.Current().GetTypeByName(className);

                Type extendedType = ExtendedTypes.GetTypeByName(className);
                if (null == extendedType)
                {
                    // Not an Extended Type
                    if (null == pdo)
                    {
                        // Try each registered assembly
                        foreach (Assembly ass in PdoAssemblies)
                        {
                            pdo = ass.CreateInstance(className) as PersistableDataObject;
                            if (null != pdo)
                            {
                                break;
                            }
                        }
                    }
                }
                else
                {
                    pdo = Activator.CreateInstance(extendedType) as PersistableDataObject;
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_CREATEFAIL", "Failed to create PersistableDataObject: {0}", className), ex);
            }

            if (null == pdo)
            {
                throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_CREATE_UNSUCCESSFUL", "Unable to create PersistableDataObject: {0}", className));
            }

            if (null != pkeyID)
            {
                pdo.Load(pkeyID); // Need to load base object to get extended class information.
                pdo = GetExtendedItem(pdo, pdo.GetType()) as PersistableDataObject;
            }

            pdo.Parent = parent;
            if (null != parent)
            {
                pdo.Session = parent.Session;
            }

            return pdo;
        }


        #endregion

        #region Public properties and methods

        /// <summary>
        /// Thread synchronization support.
        /// </summary>
        [XmlIgnore]
        public object SyncRoot
        {
            get { return _syncRoot; }
            set { _syncRoot = value; }
        }

        /// <summary>
        /// Is this object currently in the process of loading data from a datasource.
        /// </summary>
        [XmlIgnore]
        public bool LoadingFromDataSource
        {
            get { return _loadingFromDataSource; }
            set { _loadingFromDataSource = value; }
        }


        /// <summary>
        /// Is change tracking enabled for this object.  True by default.
        /// </summary>
        [XmlIgnore]
        public bool ChangeTrackingEnabled
        {
            get { return _changeTrackingEnabeled; }
            set { _changeTrackingEnabeled = value; }
        }

        /// <summary>
        /// Controls automatic expansion of contained list elements.
        /// When true, automatically expands lists to ensure expressions such as "foo.bar[3]" 
        /// have enough elements to prevent "index out of range" exceptions.
        /// </summary>
        [XmlIgnore]
        public bool AutoExpand
        {
            get
            {
                if (!_autoExpand.HasValue)
                {
                    if (Parent == null)
                    {
                        _autoExpand = false;
                    }
                    else
                    {
                        _autoExpand = Parent.AutoExpand;
                    }
                }

                return _autoExpand.Value;
            }

            set { _autoExpand = value; }
        }


        /// <summary>
        /// DataSession property
        /// </summary>
        [XmlIgnore]
        public DataSession Session
        {
            get
            {
                if (null == _session)
                {
                    _session = DataSession.DefaultSession;
                }
                return _session;
            }
            set { _session = value; }
        }

        /// <summary>
        /// Primary key for this object.
        /// </summary>
        [XmlIgnore]
        public virtual object PKeyID
        {
            get { return null; }

            // This protected set method is used via reflection when refreshing previously loaded list items.
            protected set
            {
                throw new NotImplementedException(LocalizationHelper.ErrorDefMsg("PDO_PKEYNOTDEF",
                    "PKeyID property set must be defined in derived class."));
            }

        }

        /// <summary>
        /// Primary key of base object for this derived object.
        /// </summary>
        [XmlIgnore]
        public virtual object BasePKeyID
        {
            get { return PKeyID; }
            protected set { PKeyID = value; }
        }

        /// <summary>
        /// Return database schema.table name 
        /// </summary>
        [XmlIgnore]
        public string SourceDatabaseTableName
        {
            get { return PDOMapper.GetSourceDatabaseTableName(this.GetType()); }
        }


        /// <summary>
        /// Return database schema.table name of base PersistableDataObject.
        /// </summary>
        [XmlIgnore]
        public string BaseSourceDatabaseTableName
        {
            get
            {
                Type t = this.GetType();
                while (t.BaseType != typeof(PersistableDataObject))
                {
                    t = t.BaseType;
                }
                return PDOMapper.GetSourceDatabaseTableName(t);
            }
        }

        /// <summary>
        /// Return the base type of this PersistableDataObject, i.e. the type immediately derived from PersistableDataObject.
        /// </summary>
        public Type GetBasePdoType()
        {

            Type t = this.GetType();
            while (t.BaseType != typeof(PersistableDataObject))
            {
                t = t.BaseType;
            }
            return t;

        }

        [XmlIgnore]
        public char AuditAction
        {
            get { return _auditAction; }
            set { _auditAction = value; }
        }



        /// <summary>
        /// Parent PersistableDataObject
        /// </summary>
        [XmlIgnore]
        public PersistableDataObject Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                if (null != value)
                {
                    if (value.GetHashCode().Equals(this.GetHashCode()))
                    {
                        throw new InvalidOperationException("Cannot set Parent to yourself.");
                    }
                }
                _nestingLevel = -1;  // re-initialize to -1 to force RootParent re-determiniation
                _parent = value;
            }
        }

        /// <summary>
        /// Is new?  I.e. is new if constructed but not saved.
        /// </summary>
        [XmlIgnore]
        public bool IsNew
        {
            get { return _isNew; }
            private set { _isNew = value; }
        }


        protected void SetNew()
        {
            IsNew = true;
        }

        protected void SetOld()
        {
            IsNew = false;
        }

        /// <summary>
        /// Get/Set the readonly flag.
        /// When true, properties of object cannot be changed or saved to database.
        /// </summary>
        [XmlIgnore]
        public bool IsImmutable
        {
            get { return _isImmutable; }
            internal set { _isImmutable = value; }
        }

        public void SetImmutable()
        {
            IsImmutable = true;
        }

        /// <summary>
        /// Is this object to be deleted at next save?
        /// </summary>
        [XmlIgnore]
        public bool MarkedForDeletion
        {
            get { return _markedForDeletion; }
            set
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(() => this.MarkedForDeletion), _markedForDeletion, value);

                _markedForDeletion = value;
            }
        }

        /// <summary>
        /// Containing list, or null.
        /// </summary>
        [XmlIgnore]
        internal IPersistableDataObjectList ListContainer
        {
            get { return _listContainer; }
            set
            {
                _listContainer = value;

                // CX Why?? IsDirty = true;
            }
        }

        protected void SetClean()
        {
            IsDirty = false;
        }

        /// <summary>
        /// Object dirty flag. Will always return false if IsImmutable.
        /// is true.
        /// </summary>
        [XmlIgnore]
        public bool IsDirty
        {
            get
            {
                return (IsImmutable) ? false : _isDirty;
            }
            private set
            {
                if (value == true && MarkedForDeletion)
                {
                    throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_CHGNOTALLOWED",
                        "Changes are not allowed to objects marked for deletion."));
                }

                _isDirty = value;
            }
        }


        /// <summary>
        /// Set object state to dirty.  No notifications are sent.
        /// </summary>
        protected void SetDirty()
        {
            IsDirty = true;
        }


        /// <summary>
        /// Mark this list containing object as dirty.
        /// </summary>
        internal void MarkListContentsDirty()
        {
            // All this method does is ensure a new parent PDO is marked dirty when adding or removing 
            // elements to child lists. It is possible for a parent to contain nothing but lists, in 
            // which case this is the only way to get the parent saved.
            if (IsNew)
            {
                IsDirty = true;
            }
        }


        /// <summary>
        /// Set overall object as dirty, send dirty notification for specific property.
        /// </summary>
        /// <param name="propertyName">name of dirty property</param>
        protected void SetDirty(string propertyName)
        {
            IsDirty = true;
            OnPropertyChanged(propertyName);
        }

        /// <summary>
        /// Set overall object as dirty, send dirty notification for specific property.
        /// </summary>
        /// <param name="propertyName">expression giving name of dirty property</param>
        protected void SetDirty(Expression<Func<object>> expression)
        {
            SetDirty(PDOMapper.GetNotificationPropertyName(expression));
        }


        /// <summary>
        /// Returns unique identifiying key useful for dictionary lookups.
        /// </summary>
        [XmlIgnore]
        public virtual string DBKey
        {
            get { return PDOMapper.GetDBKey(this, PKeyID); }
            // OPTIMIZATION NOTE: Consider replacing this with an override of GetHashCode().
        }


        /// <summary>
        /// Returns a key string derived from the database table name of this 
        /// object's base class and the corresponding primary key for that base class.
        /// </summary>
        [XmlIgnore]
        public string BaseDBKey
        {
            get
            {
                Guid g = (Guid)this.BasePKeyID;
                if (g.IsNullOrEmpty())
                {
                    string key = this.BaseSourceDatabaseTableName + ":" + this.PKeyID;
                    return key;
                }

                return this.BaseSourceDatabaseTableName + ":" +
                    (this.BasePKeyID == null ? this.PKeyID : this.BasePKeyID);
            }
        }


        /// <summary>
        /// Controls in-memory caching and timing of actual persistance to database.
        /// </summary>
        [XmlIgnore]
        public virtual bool IsEthereal
        {
            get { return _isEthereal; }
            set { _isEthereal = value; }
        }

        #endregion

        #region Load from data source

        /// <summary>
        /// Re-Load from database.  Used primarily by notification system.
        /// </summary>
        /// parent, false for normal change notifications</param>
        public void Refresh()
        {

            if (null == PKeyID)
            {
                throw new ConstraintException(LocalizationHelper.ErrorDefMsg("PDO_PKEYNOTSET", "PKeyID must be set."));
            }


            object pkeyId = PKeyID; // save before calling Initialize which resets it.

            Initialize();
            Load(pkeyId);

            // TODO: -- Optimization Note --
            // Initialize() initializes all member variables, PDO object references and PDO list references, 
            // which forces PDO objects and lists to reload from the database on next reference.
            // Lists need to be force refreshed in order to reflect add/remove changes.
            // It would be possible to add a separate flag to track when a list needs to be updated without 
            // forcing a full refresh.
        }


        /// <summary>
        /// Set all properties to default values and clear all lazy loaded cached objects and lists.
        /// Code generated classes contain concrete implmentation.
        /// Derived classes must call base.
        /// </summary>
        virtual protected void Initialize()
        {
            IsNew = true;
            IsDirty = false;
        }

        /// <summary>
        /// Provides opportunity to override in concrete implmentation.
        /// Derived classes must call base.
        /// </summary>
        virtual public void PrepareLoad()
        {
            // MIGRATE moved to LoadFromDataRow base -- MarkedForDeletion = false;
        }

        /// <summary>
        /// Provides opportunity to override in concrete implmentation.
        /// NOTE: This method is called only as part of the SaveAll() operations, not for a simple Save().
        /// </summary>
        virtual public void PrepareSave()
        {
            // no-op in base class
        }

        /// <summary>
        /// Called by PersistableDataObjectXmlSerializer prior to serialization.
        /// Provides opportunity to add items to IncludePropertyPath, ExcludeType, etc.
        /// </summary>
        virtual public void PrepareSerialization()
        {
            // no-op in base class
        }

        /// <summary>
        /// Load from database using provided sql string instead of normal load behavior.  If sql returns more than
        /// one record, exception is thrown.
        /// </summary>
        /// <param name="sql">sql select statement</param>
        /// <returns>true for success, false otherwise</returns>
        public bool LoadFromSQL(string sql)
        {
            DataSet ds = Session.ExecuteDataset(CommandType.Text, sql);

            if (ds.Tables[0].Rows.Count < 1)
            {
                IsNew = true;
                return false;
            }
            else if (ds.Tables[0].Rows.Count > 1)
            {
                throw new PersistableDataObjectException(LocalizationHelper.ErrorDefMsg("PDO_MULTIMATCHSQL",
                    "Multiple matching records were found in LoadFromSQL"));
            }

            PrepareLoad();

            LoadFromDataRow(ds.Tables[0].Rows[0]);

            PostLoadInitialize();

            return !IsNew;
        }





        /// <summary>
        /// Try to load object from provided LINQ query.
        /// </summary>
        /// <param name="query">query returning a single result.</param>
        /// <returns>true if successful, false otherwise</returns>
        public bool TryLoad(IQueryable query)
        {
            return CoreLoad(query, false);
        }


        /// <summary>
        /// Load object from provided LINQ query.
        /// 
        /// Exceptions:
        ///   InvalidOperationException:
        ///     If other than exactly 1 matching record is found.
        /// </summary>
        /// <param name="query">query returning a single result.</param>
        public void Load(IQueryable query)
        {
            CoreLoad(query, true);
        }


        /// <summary>
        /// Attempt to load object from provided LINQ query, generating exceptions or 
        /// returning success bool as determined by exceptOnError param
        /// 
        /// Exceptions (conditional):
        ///   InvalidOperationException:
        ///     If other than exactly 1 matching record is found.
        /// </summary>
        /// <param name="query">query returning a single result.</param>
        /// <returns>true if successful, false otherwise</returns>
        private bool CoreLoad(IQueryable query, bool exceptOnError)
        {
            DataRequest dr = new DataRequest(DataRequest.DatabaseTarget.Automatic);//Session.SelectedDatabaseTarget);

            System.Data.Common.DbCommand cmd = DataSession.LINQDataContext.GetCommand(query);

            if (cmd.Parameters.Count > 0)
            {
                dr.Parameters = new List<object>();
                foreach (System.Data.Common.DbParameter param in cmd.Parameters)
                {
                    dr.Parameters.Add(new SqlParameter(param.ParameterName, param.Value));
                }
            }

            dr.SqlCommandString = query.ToString();
            DataSet ds = Session.ExecuteDataset(dr);

            if (ds.Tables[0].Rows.Count < 1)
            {
                if (exceptOnError)
                    throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_NOTFOUND", "Requested record does not exist."));
                else
                    return false;
            }
            else if (ds.Tables[0].Rows.Count > 1)
            {
                if (exceptOnError)
                    throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_MULTIMATCHLOAD", "Unexpected multiple matching records were found in Load."));
                else
                    return false;
            }

            PrepareLoad();

            LoadFromDataRow(ds.Tables[0].Rows[0]);

            PostLoadInitialize();

            return true;
        }


        /// <summary>
        /// Load instance from sql.
        /// Special purpose only - use with care.
        /// </summary>
        public bool LoadUnSafe(DataSession session, string sql)
        {
            Session = session;
            DataSet ds = Session.ExecuteDataset(CommandType.Text, sql);
            if (ds.Tables[0].Rows.Count < 1)
            {
                throw new PersistableDataObjectException(LocalizationHelper.ErrorDefMsg("PDO_NOTFOUND", "No record found."));
            }
            else if (ds.Tables[0].Rows.Count > 1)
            {
                throw new PersistableDataObjectException(LocalizationHelper.ErrorDefMsg("PDO_MULTIMATCH", "Multiple records found."));
            }

            IsNew = false;

            PrepareLoad();

            LoadFromDataRow(ds.Tables[0].Rows[0]);

            PostLoadInitialize();

            return !IsNew;
        }



        // overriden in generated / derived class


        /// <summary>
        /// Load (hydrate) a PersistableDataObject from a provided DataRow.
        /// Overriden in generated / derived class.
        /// </summary>
        /// <param name="dr">DataRow from which to load.</param>
        public virtual void LoadFromDataRow(DataRow dr)
        {
            MarkedForDeletion = false;
            IsNew = false;
            IsDirty = false;

            EventsHelper.Fire(LoadComplete, this);

        }

        /// <summary>
        /// Load (hydrate) a PersistableDataObject from a provided primary key value.
        /// Overriden in generated / derived class.
        /// </summary>
        /// <param name="pkeyID">primary key of object to load.</param>
        /// <returns>true if valid, false otherwise.</returns>
        public bool Load(object pkeyID)
        {
            bool success = Read(pkeyID);

            if (success)
            {
                return PostLoadInitialize(); // or OnCreated
            }
            else
            {
                return false;
            }
        }


        /// <summary>
        /// Perform any post-load object initialization.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        public virtual bool PostLoadInitialize()
        {
            if (null != Initializing)
            {
                Initializing(this, new EventArgs());
            }

            RootParent.AddToSubscriptionChain(this);

            IsNew = false;
            SetClean();

            return true; // no failure path currently defined - always return true
        }

        public static Type GetTypeByName(string fullTypeName)
        {
            Type extendedType = ExtendedTypes.GetTypeByName(fullTypeName);

            if (null == extendedType)
            {
                PersistableDataObject pdo = null;

                // Try each registered assembly
                foreach (Assembly ass in PdoAssemblies)
                {
                    pdo = ass.CreateInstance(fullTypeName) as PersistableDataObject;
                    if (null != pdo)
                    {
                        break;
                    }
                }

                if (null != pdo)
                {
                    return pdo.GetType();
                }
                return null;
            }
            else
            {
                return extendedType;
            }
        }



        /// <summary>
        /// Create an extended item from provided base item and base type, if an extension is available.
        /// </summary>
        /// <typeparam name="T">base type</typeparam>
        /// <param name="item">item to extend</param>
        /// <returns>new type item if extended; original item otherwise</returns>
        public static object GetExtendedItem(PersistableDataObject baseItem, Type baseType)
        {

            // NOTE: Optimize by caching TypeExtensionAttribute info.
            TypeExtensionAttribute attr = ReflectionHelper.GetAttribute<TypeExtensionAttribute>(baseType);
            if (null != attr)
            {
                string extendedClassName = ReflectionHelper.GetObjectPropertyValue(baseItem, attr.ExtendedClassNameProperty) as string;

                Type extendedType = ExtendedTypes.GetTypeByName(extendedClassName);
                if (extendedType != default(Type))
                {
                    PersistableDataObject extendedItem = (PersistableDataObject)Activator.CreateInstance(extendedType);

                    string sqlQuery = string.Empty;
                    string args = PDOMapper.GetSqlSelectParams(extendedType);


                    string baseTable = baseItem.SourceDatabaseTableName;
                    string topTable = extendedItem.SourceDatabaseTableName;


                    // CX - TEMP - needs clean-up following confirmation of functionality for Hydra

                    if (extendedType.BaseType != baseType)
                    {

                        MetaModel mm = DataSession.LINQDataContext.Mapping.MappingSource.GetModel(baseType);

                        string fromClause = " from " + topTable;
                        string joinClause = string.Empty;

                        Type midType = ExtendedTypes.GetTypeByName(extendedType.BaseType.Name);
                        PersistableDataObject midItem = (PersistableDataObject)Activator.CreateInstance(midType);
                        string midArgs = PDOMapper.GetSqlSelectParams(midType);
                        string midTable = midItem.SourceDatabaseTableName;
                        TypeExtensionAttribute midAttr = ReflectionHelper.GetAttribute<TypeExtensionAttribute>(midType);

                        string pkeyName = mm.GetMetaType(baseType).IdentityMembers[0].Name;
                        args += ", " + midArgs;
                        args += ", " + PDOMapper.GetSqlSelectParams(baseType);

                        joinClause += " join " + midTable + " on " + midTable + "." + mm.GetMetaType(midType).IdentityMembers[0].Name + " = " +
                           topTable + ".[" + midAttr.BaseItemPKeyProperty + "]";

                        joinClause += " join " + baseTable + " on " + baseTable + "." + mm.GetMetaType(baseType).IdentityMembers[0].Name + " = " +
                           midTable + ".[" + attr.BaseItemPKeyProperty + "]";

                        // NOTE: This sql presumes we're using a Guid primary key
                        string whereClause = " where " + baseTable + ".[" + mm.GetMetaType(baseType).IdentityMembers[0].Name + "] = '" +
                            baseItem.PKeyID.ToString() + "'";

                        sqlQuery = "select " + args + fromClause + joinClause + whereClause;
                    }
                    else
                    {
                        // NOTE: This sql presumes we're using a Guid primary key
                        Debug.Assert(baseItem.PKeyID is Guid);
                        string oldSqlQuery = string.Format(System.Globalization.CultureInfo.InvariantCulture,
                            "select {0} from {1} where [{2}] = '{3}'", args,
                             topTable, attr.BaseItemPKeyProperty, baseItem.PKeyID.ToString());


                        MetaModel mm = DataSession.LINQDataContext.Mapping.MappingSource.GetModel(baseType);

                        string fromClause = " from " + topTable;
                        string joinClause = string.Empty;


                        string pkeyName = mm.GetMetaType(baseType).IdentityMembers[0].Name;
                        args += ", " + PDOMapper.GetSqlSelectParams(baseType);


                        joinClause += " join " + baseTable + " on " + baseTable + "." + mm.GetMetaType(baseType).IdentityMembers[0].Name + " = " +
                           topTable + ".[" + attr.BaseItemPKeyProperty + "]";

                        // NOTE: This sql presumes we're using a Guid primary key
                        string whereClause = " where " + baseTable + ".[" + mm.GetMetaType(baseType).IdentityMembers[0].Name + "] = '" +
                            baseItem.PKeyID.ToString() + "'";

                        sqlQuery = "select " + args + fromClause + joinClause + whereClause;


                        if (!sqlQuery.Equals(oldSqlQuery))
                        {
                            int foo = 2;
                        }
                    }


                    if (extendedItem.LoadFromSQL(sqlQuery))
                    {
                        extendedItem.Session = baseItem.Session;
                        extendedItem.Parent = baseItem.Parent;

                        return GetExtendedItem(extendedItem, extendedItem.GetType());
                    }
                    else
                    {
                        throw new PersistableDataObjectException((LocalizationHelper.ErrorDefMsg("PDO_EXTYPE_NOTFOUND", "Required information for ExtendedType \"{0}\" not found in database.", extendedType.Name)));
                    }
                }
            }

            return baseItem;
        }

        /// <summary>
        /// Create an extended item from provided base item, if an extension is available.
        /// </summary>
        /// <typeparam name="T">base type</typeparam>
        /// <param name="item">item to extend</param>
        /// <returns>new type item if extended; original item otherwise</returns>
        private static T GetExtendedItem<T>(T item) where T : PersistableDataObject
        {
            return GetExtendedItem(item, typeof(T)) as T;
        }

        #endregion

        #region Notification support

        /// <summary>
        /// Add subscription to provided pdo through event chain.
        /// </summary>
        /// <param name="pdo"></param>
        internal void AddToSubscriptionChain(IPersistableData pdo)
        {
            if (null != SubscriptionUpdater)
            {
                EventsHelper.FireAsync(SubscriptionUpdater, pdo, null);
            }
        }

        #endregion

        #region List support

        /// <summary>
        /// Return a generic list of PersistableDataObject derived objects.
        /// </summary>
        /// <typeparam name="T">type of object in list</typeparam>
        /// <param name="parent">parent object</param>
        /// <param name="parentForeignKeyName">name of foreign key property on parent object</param>
        /// <param name="session">session object</param>
        /// <param name="sprocName">stored procedure name</param>
        /// <returns></returns>
        public static PersistableDataObjectList<T> GetList<T>(IQueryable query)
                                    where T : PersistableDataObject, new()
        {
            return PDOMapper.GetList<T>(null, null, null, DataSession.DefaultSession, query);
        }

        /// <summary>
        /// Return a generic list of PersistableDataObject derived objects.
        /// </summary>
        /// <typeparam name="T">type of object in list</typeparam>
        /// <param name="parent">parent object</param>
        /// <param name="parentForeignKeyName">name of foreign key property on parent object</param>
        /// <param name="session">session object</param>
        /// <param name="sprocName">stored procedure name</param>
        /// <returns></returns>
        public static PersistableDataObjectList<T> GetList<T>(IQueryable query, DataSession session)
                                    where T : PersistableDataObject, new()
        {
            return PDOMapper.GetList<T>(null, null, null, session, query);
        }


        /// <summary>
        /// Return a single item from list where named item property matches value.
        /// Throws an InvalidOperationException if duplicates are found.
        /// </summary>
        /// <typeparam name="T">type of object in list</typeparam>
        /// <param name="list">source list</param>
        /// <param name="property">property on list item</param>
        /// <param name="value">item value to match</param>
        /// <returns>matching item from list, or null</returns>
        public static T GetListItem<T>(IList list, string property, object value)
                where T : PersistableDataObject
        {
            return GetListItem<T>(list, property, value, true);
        }


        /// <summary>
        /// Return a single item from list where named property matches value, optionally checking 
        /// for duplicate item matching.
        /// </summary>
        /// <typeparam name="T">type of object in list</typeparam>
        /// <param name="list">source list</param>
        /// <param name="property">property on list item</param>
        /// <param name="value">item value to match</param>
        /// <param name="checkDuplicates">true to throw ArgumentException if duplicates found, false to omit duplicate check</param>
        /// <returns>matching item from list, or null</returns>
        public static T GetListItem<T>(IList list, string property, object value, bool checkDuplicates)
                where T : PersistableDataObject
        {
            T selectedItem = null;

            object[] argValues = { };
            MethodInfo mi = null;

            foreach (T item in list)
            {
                if (null == mi)
                {
                    mi = ReflectionHelper.GetPropertyInfo(item, property).GetGetMethod();
                }

                if (value.ToString().Equals(mi.Invoke(item, argValues).ToString()))
                {
                    if (checkDuplicates && selectedItem != null)
                    {
                        throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_MULTIFIND", "Multiple items found with same key property value; {0} = {1}", property, value.ToString()));
                    }

                    selectedItem = item;

                    if (!checkDuplicates)
                    {
                        break;
                    }
                }
            }
            return selectedItem as T;
        }


        /// <summary>
        /// Return a list subset from provided list where named property matches value.
        /// </summary>
        /// <typeparam name="T">type of object in list</typeparam>
        /// <param name="list">source list</param>
        /// <param name="property">property on list item</param>
        /// <param name="value">item value to match</param>
        /// <returns>matching item from list, or null</returns>
        public static List<T> GetSubList<T>(IList list, string property, object value)
                where T : PersistableDataObject
        {
            List<T> subList = new List<T>();

            foreach (T item in list)
            {
                if (value.ToString() == ReflectionHelper.GetObjectPropertyValue(item, property).ToString())
                {
                    subList.Add(item);
                }
            }
            return subList;
        }
        #endregion

        #region Validation

        /// <summary>
        /// Default condition is true.
        /// Value of Valid is always set to true at start of validation process.
        /// </summary>
        [XmlIgnore]
        public virtual bool Valid
        {
            get { return _isValid; }
            private set { _isValid = value; }
        }

        /// <summary>
        /// Return true if in a valid condition.  
        /// </summary>
        /// <returns>true if valid, false otherwise.</returns>
        public virtual bool Validate()
        {
            ValidationStatus = (this as IDataErrorInfo).Error;
            Valid = string.IsNullOrEmpty(ValidationStatus);

            RootParent.UpdateValidationErrors(); // notify top-level parent of updated validation error info.
            return Valid;
        }

        /// <summary>
        /// Last validation status as string. 
        /// Does not invoke checking of any validation rules, merely returns last known status.
        /// </summary>
        [XmlIgnore]
        public string ValidationStatus
        {
            get { return _validationStatus; }
            set { SetString(ref _validationStatus, value, () => this.ValidationStatus); }
        }

        #endregion

        #region Save (insert/update)


        /// <summary>
        /// Save this object, and any child objects of type PersistableDataObject in a transaction.
        /// </summary>
        /// <returns>true for succecss, false otherwise.</returns>
        private bool SaveAllTransacted()
        {
            bool success = false;

            Memento rollback = MementoManager.CreateMemento(this);

            using (TransactionScope tx = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(0, 0, 5)))
            {
                try
                {
                    success = SaveAllCore();
                }
                catch (Exception ex)
                {
                    throw (new TransactionException(LocalizationHelper.ErrorDefMsg("PDO_SAVEALLTRANS", "SaveAll failed with exception; transaction rolled back."), ex));
                }
                finally
                {
                    if (success)
                    {
                        tx.Complete();
                        MementoManager.ReleaseMemento(rollback);
                    }
                    else
                    {
                        MementoManager.Undo();
                    }
                }
            }
            return success;
        }

        /// <summary>
        /// Save this object, all child objects and child object lists.
        /// </summary>
        /// <param name="transacted">true to wrap in a transaction</param>
        /// <returns>true for success, false otherwise</returns>
        public bool SaveAll(bool transacted)
        {
            if (transacted)
            {
                return SaveAllTransacted();
            }
            else
            {
                return SaveAll();
            }
        }

        /// <summary>
        /// Save this object, all child objects and child object lists.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        public virtual bool SaveAll()
        {
            return SaveAllCore();
        }


        /// <summary>
        /// Get a FieldInfo array from provided PDO and PersistableDataObject derived base classes.
        /// </summary>
        /// <param name="pdo"></param>
        /// <returns></returns>
        protected static FieldInfo[] GetHierarchyFields(PersistableDataObject pdo, bool includeNonPersisted)
        {

            List<FieldInfo> fiHierarchy = new List<FieldInfo>();
            Type pdoType = pdo.GetType();

            while (pdoType != typeof(PersistableDataObject))
            {

                // Use private fields instead of public properties, since accessing the property
                // for a lazy-loaded child object will cause a database load or create an empty object.
                FieldInfo[] fiarray = pdoType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

                foreach (FieldInfo fi in fiarray)
                {
                    // Skip over fields marked with attribute signaling non-participation in persistance operations,
                    // unless we have explicitly asked to include them.
                    bool include = includeNonPersisted;

                    if (!include)
                    {
                        object[] npa = fi.GetCustomAttributes(typeof(NonPersistedAttribute), false);
                        include = (npa.Length == 0);
                    }

                    if (include)
                    {
                        fiHierarchy.Add(fi);
                    }
                }

                pdoType = pdoType.BaseType;
            }
            return fiHierarchy.ToArray();
        }


        /// <summary>
        /// Get a PropertyInfo array for objects that are managed as part of the BeginEdit, EndEdit, CancelEdit 
        /// cycle.  Source and destination PersistableDataObject are the original and modified data context objects.
        /// </summary>
        /// <param name="source">source</param>
        /// <param name="destination">destiantion</param>
        /// <returns>PropertyInfo array of properties to copy values from / to.</returns>
        protected static PropertyInfo[] GetCopyProperties(PersistableDataObject source, PersistableDataObject destination)
        {
            Type pdoType = source.GetType(); // start with provided type, and recurse down inheritance chain

            Debug.Assert(pdoType.Equals(destination.GetType()));

            List<PropertyInfo> piHierarchy = new List<PropertyInfo>();

            while (pdoType != typeof(PersistableDataObject)) // loop until we hit the base type
            {
                FieldInfo[] fia = pdoType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);

                PropertyInfo[] pia = pdoType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                foreach (PropertyInfo p in pia)
                {
                    if (!p.CanWrite)
                        continue; // not settable

                    if (p.GetCustomAttributes(typeof(System.Xml.Serialization.XmlIgnoreAttribute), true).Length != 0)
                        continue; // marked as XmlIgnore


                    // Get LINQ backing field for this property, if it exists.

                    // NOTE: ColumnAttribute and AssociationAttribute share common DataAttribute base.
                    DataAttribute[] daa = p.GetCustomAttributes(typeof(DataAttribute), true) as DataAttribute[];
                    DataAttribute da = ((daa.Length == 1) ? daa[0] : null);

                    FieldInfo backingField = null;
                    if (da != null)
                    {
                        backingField = (from f in fia where f.Name == da.Storage select f).FirstOrDefault<FieldInfo>();
                    }

                    if (backingField != null) // this property is database persisted 
                    {
                        object[] npa = backingField.GetCustomAttributes(typeof(NonPersistedAttribute), false);

                        if (npa.Length == 0) // not marked as NonPersisted
                        {
                            // If value of backing field for *either* source or destination is non-null we're going to copy values.
                            object sv = backingField.GetValue(source);
                            object dv = backingField.GetValue(destination);
                            if (sv != null || dv != null)
                            {
                                piHierarchy.Add(p);
                            }
                        }
                    }
                    else if (typeof(PersistableDataObject).IsAssignableFrom(p.PropertyType))
                    {
                        piHierarchy.Add(p);
                    }
                }

                pdoType = pdoType.BaseType; // recurse down inheritance chain
            }
            return piHierarchy.ToArray();
        }


        /// <summary>
        /// Call the PrepareSave method on this and any child objects.
        /// </summary>
        private void PrepareSaveCore(FieldInfo[] fiarray)
        {
            // First, call PrepareSave on any children (on the "one" side of a one-to-many relationship).
            foreach (FieldInfo fi in fiarray)
            {
                object obj = fi.GetValue(this);
                {
                    PersistableDataObject pdo = obj as PersistableDataObject;
                    if (null != pdo)
                    {
                        pdo.PrepareSave();
                    }
                }
            }

            // Call PrepareSave on ourself.
            this.PrepareSave();

            // Check for any children PersistableDataObjectLists and call PrepareSave on them.
            foreach (FieldInfo fi in fiarray)
            {
                object obj = fi.GetValue(this);
                if (obj != null)
                {
                    Type oType = obj.GetType();
                    if (oType.IsGenericType)
                    {
                        Type[] args = oType.GetGenericArguments();

                        if (args.Length == 1 &&
                            typeof(PersistableDataObject).IsAssignableFrom(args[0]))
                        {
                            IPersistableDataObjectList list = obj as IPersistableDataObjectList;
                            if (null != list)
                            {
                                foreach (PersistableDataObject pdo in list)
                                {
                                    pdo.PrepareSave();
                                }
                            }
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Save this object, and any child objects of type PersistableDataObject.
        /// </summary>
        /// <returns>true for succecss, false otherwise.</returns>
        private bool SaveAllCore()
        {
            bool success = true;

            FieldInfo[] fiarray = GetHierarchyFields(this, false);

            PrepareSaveCore(fiarray);

            // First, save any children (on the "one" side of a one-to-many relationship) which
            // updates foreign keys in this (parent) object.
            foreach (FieldInfo fi in fiarray)
            {
                object obj = fi.GetValue(this);
                if (success && obj != null)
                {
                    PersistableDataObject pdo = obj as PersistableDataObject;
                    if (null != pdo)
                    {
                        if (Parent != null && (Parent.GetHashCode().Equals(pdo.GetHashCode())))
                        {
                            // Do not attempt to save Parent which produces recursive callback
                            continue;
                        }

                        success = pdo.SaveAll();
                    }
                }
            }

            // Save this (parent) object, which updates our primary key if this is a new object.
            if (success)
            {
                success = this.Save();
            }

            // Finally, save any list items (on the "many" side of a one-to-many relationship).

            // Check for any children PersistableDataObjectLists and update their foreign key references,
            // then save the list elements.
            foreach (FieldInfo fi in fiarray)
            {
                object obj = fi.GetValue(this);
                if (success && obj != null)
                {
                    Type oType = obj.GetType();
                    if (oType.IsGenericType)
                    {
                        Type[] args = oType.GetGenericArguments();

                        if (args.Length == 1 &&
                            typeof(PersistableDataObject).IsAssignableFrom(args[0]))
                        {
                            IPersistableDataObjectList list = obj as IPersistableDataObjectList;
                            if (null != list)
                            {
                                foreach (PersistableDataObject pdo in list)
                                {
                                    pdo.RaiseForeignKeyUpdateEvent(PKeyID);
                                }
                                list.Save();
                            }
                        }
                    }
                }
            }
            return success;
        }


        /// <summary>
        /// Compare all database defined property values between provided object and this.
        /// </summary>
        /// <param name="source">pdo to compare against</param>
        /// <returns>true if all compared values are equal, false otherwise.</returns>
        private bool EqualPersistedValues(object source)
        {
            Debug.Assert(source.GetType().IsAssignableFrom(this.GetType()));

            Type pdoType = source.GetType();

            // All database persisted properties are defined in the FieldNames struct in generated 
            // PersistableDataObject derived classes.
            Type ft = pdoType.GetNestedType("FieldNames");

            if (null != ft) // only null for non-generated PersistableDataObjects (hand-made) that don't have any FieldNames defined
            {
                FieldInfo[] sourceField = ft.GetFields(BindingFlags.Public | BindingFlags.Static);

                // Compare each source value to correspondingly named target field value
                foreach (FieldInfo sfi in sourceField)
                {
                    string v1 = ReflectionHelper.GetObjectPropertyValue(source, sfi.Name, string.Empty).ToString();
                    string v2 = ReflectionHelper.GetObjectPropertyValue(this, sfi.Name, string.Empty).ToString();
                    if (!string.Equals(v1, v2))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Set save state for imported PDOs by checking for differences from already existing entity, if present.
        /// </summary>
        /// <param name="pdo">pdo being imported.</param>
        private void SetSaveState(PersistableDataObject pdo)
        {
            Type pdoType = pdo.GetType();

            PersistableDataObject comparePdo = (PersistableDataObject)Activator.CreateInstance(pdoType);

            if (comparePdo.Load(pdo.PKeyID))
            {
                // PDO exists in database - compare data field values.
                if (!pdo.EqualPersistedValues(comparePdo))
                {
                    // Data fields don't match, set save state to perform an Update.
                    pdo.IsDirty = true;
                    pdo.IsNew = false;
                }
                else
                {
                    // Matches current database entity, so no need to save anything.
                    pdo.SetClean();
                    pdo.IsNew = false;
                }
            }
            else
            {
                // Doesn't exist at all in database, set save state to perform an Insert.
                if (pdo.IsDirty)
                {
                    pdo.IsNew = true;
                    pdo.IsDirty = true;
                }
            }

        }


        /// <summary>
        /// Import this into database by either updating or inserting as neccessary.
        /// </summary>
        /// <returns>true if successful, false otherwise.</returns>
        public bool Import()
        {
            SetSaveState(this);

            return this.Save();
        }

        /// <summary>
        /// Import this, and all child items, into database by either updating or inserting as neccessary.
        /// </summary>
        /// <returns>true if successful, false otherwise.</returns>
        public bool ImportAll()
        {
            bool success = true;

            // First, import any children (on the "one" side of a one-to-many relationship) which
            // updates foreign keys in this (parent) object.
            FieldInfo[] fiarray = GetHierarchyFields(this, false);
            foreach (FieldInfo fi in fiarray)
            {
                object obj = fi.GetValue(this);
                if (success && obj != null)
                {
                    PersistableDataObject pdo = obj as PersistableDataObject;
                    if (null != pdo)
                    {
                        success = pdo.ImportAll();
                    }
                }
            }

            // Save this (parent) object.
            if (success)
            {
                Import();
            }

            // Finally, save any list items (on the "many" side of a one-to-many relationship).

            // Check for any children PersistableDataObject lists and update their foreign key references,
            // then save the list elements.
            foreach (FieldInfo fi in fiarray)
            {
                object obj = fi.GetValue(this);
                if (success && obj != null)
                {
                    Type oType = obj.GetType();
                    if (oType.IsGenericType)
                    {
                        Type[] args = oType.GetGenericArguments();

                        if (args.Length == 1 &&
                            typeof(PersistableDataObject).IsAssignableFrom(args[0]))
                        {
                            IPersistableDataObjectList list = obj as IPersistableDataObjectList;
                            if (null != list)
                            {
                                list.Import();
                            }
                        }
                    }
                }
            }
            return success;
        }

        /// <summary>
        /// Set Audit information if audit properties are present (using Reflection).
        /// </summary>
        private void SetAuditInfo()
        {
            if (!IsDirty)
            {
                // We don't want to save empty objects, so don't set audit information 
                // on new PDO's that are unmodified (i.e. not dirty).  
                // Setting audit info would actually *make* this object dirty and 
                // force a save, which we do not want.
                return;
            }

            if (Session.UserGuid.IsNullOrEmpty())
            {
                // CX
                // Set userguid to test user if none found.
                // This typically
                Session.UserGuid = new Guid("f7787568-8a49-408f-82a1-0f7d2952c904"); // CX TEMP
                Debug.WriteLine("PDO : No UserGuid found on Session; supplying TestUser default value.");
            }

            const string CREATED_BY = "CreatedBy";
            const string CREATED_ON = "CreatedOn";
            const string EDITED_BY = "EditedBy";
            const string EDITED_ON = "EditedOn";

            if (ReflectionHelper.IsProperty(this, CREATED_BY) &&
                ReflectionHelper.IsProperty(this, CREATED_ON))
            {
                string createdBy = ReflectionHelper.GetObjectPropertyValue(this, CREATED_BY, string.Empty).ToString();
                if (string.Empty.Equals(createdBy) || Guid.Empty.Equals(new Guid(createdBy)))
                {
                    ReflectionHelper.SetObjectPropertyValue(this, CREATED_BY, Session.UserGuid);
                    ReflectionHelper.SetObjectPropertyValue(this, CREATED_ON, DateTime.Now);
                }
                else if (IsDirty &&
                    ReflectionHelper.IsProperty(this, EDITED_BY) &&
                    ReflectionHelper.IsProperty(this, EDITED_ON))
                {
                    ReflectionHelper.SetObjectPropertyValue(this, EDITED_BY, Session.UserGuid);
                    ReflectionHelper.SetObjectPropertyValue(this, EDITED_ON, DateTime.Now);
                }
            }
        }


        /// <summary>
        /// Persist changes made to this object to database by performing a Delete, Insert or Update.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        public virtual bool Save()
        {
            bool success = true;
            bool savingChanges = false;

            if (IsImmutable)
            {
                return success;
            }

            if (MarkedForDeletion)
            {
                savingChanges = true;
                success = Delete();

                Initialize(); // clear dependent object references
            }
            else
            {
                if (_isDirty)
                {
                    SetAuditInfo();

                    savingChanges = true;

                    if (_isNew)
                    {
                        success = Insert();

                        if (null != PrimaryKeyUpdateHandler)
                        {
                            PrimaryKeyUpdateHandler(PKeyID);
                        }

                        if (success)
                        {
                            IsNew = false;
                        }

                        // For new members, subscribe to notifications, etc. only after the item has been saved
                        // and it therefore has a valid primary key on which to base subscriptions.
                        RootParent.AddToSubscriptionChain(this);
                    }
                    else
                    {
                        success = Update();


                        // CX - Hydra test 
                        if (null != PrimaryKeyUpdateHandler)
                        {
                            PrimaryKeyUpdateHandler(PKeyID);
                        }
                    }
                }
            }

            if (success)
            {
                IsDirty = false;
                MarkedForDeletion = false;
            }

            return success;
        }


        /// <summary>
        /// Read a record.  
        /// Not exposed as public; use public Load.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        protected virtual bool Read(object pkeyID)
        {
            return false; // no-op
        }


        /// <summary>
        /// Insert new record.  
        /// Not exposed as public; use Save to either Insert or Update as appropriate.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        protected virtual bool Insert()
        {
            return true; // no-op
        }


        /// <summary>
        /// Update existing record. 
        /// Not exposed as public; use Save to either Insert or Update as appropriate.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        protected virtual bool Update()
        {
            return true; // no-op
        }


        /// <summary>
        /// Remove an existing record. 
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        protected virtual bool Purge()
        {
            return true; // no-op
        }

        /// <summary>
        /// Remove an existing record.
        /// </summary>
        /// <returns>true for success, false otherwise</returns>
        public bool Delete()
        {
            bool result = Purge();

            if (Transaction.Current == null)
            {
                //   NotifyObjectChanged();
            }

            return result;
        }

        public virtual bool Sync()
        {
            return true;
        }

        #endregion

        #region Typed property set methods

        private void CheckReadOnly()
        {
            if (!LoadingFromDataSource && IsImmutable)
            {
                throw new InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_READONLY", "Attempt to change read-only instance of {0}", this.GetType().Name));
            }
        }

        /// <summary>
        /// Chain validation events from provided child PDO up this PDO.
        /// </summary>
        /// <param name="pdo">child PDO</param>
        virtual public void ChainValidationEvents(PersistableDataObject childPdo)
        {
            childPdo.ValidationChanged += new ValidationChangedEventHandler(pdo_ValidationChange);
        }


        void pdo_ValidationChange(object sender, ValidationEventArgs e)
        {
            if (null != _memento) // only send validation change messages when after a BeginEdit
            {
                ////ValidationInfo mi = new ValidationInfo(sender.GetType().Name);
                SetValidationResult(sender.GetType().GetProperty("PKeyID"), e.ErrorSummary);

                if (null != ValidationChanged)
                {
                    ValidationChanged(sender, e);
                }
            }
        }

        protected enum ChangeTracking
        {
            On,
            Off
        };
        PropertyChangedEventHandler _propertyChangedHandler;

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetPDO<T>(ref T pdoOut, T pdoTo, Expression<Func<object>> propertyExpression)
            where T : PersistableDataObject
        {
            return SetPDO<T>(ref pdoOut, pdoTo, propertyExpression, ChangeTracking.On);
        }

        /// <summary>
        /// Set pdoOut to pdoTo if they are different. 
        /// The PropertyChanged event is fired, but the dirty flag is NOT set to true when changed, 
        /// since the pdo managed through this property is not a true child of the containing pdo.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetPDO<T>(ref T pdoOut, T pdoTo, Expression<Func<object>> propertyExpression, ChangeTracking trackChanges)
            where T : PersistableDataObject
        {

            CheckReadOnly();

            if (pdoOut == null || pdoTo == null || (pdoOut.GetHashCode() != pdoTo.GetHashCode()))
            {
                if (pdoOut != null)
                {
                    if (_propertyChangedHandler != null)
                    {
                        // Remove event handler on previously subscribed pdo.
                        pdoOut.PropertyChanged -= _propertyChangedHandler;
                    }
                }

                if (trackChanges == ChangeTracking.On && ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), pdoOut, pdoTo);

                pdoOut = pdoTo;

                if (null != pdoOut)
                {
                    // Set event handler on child to notify this (the pdoOut's change subscriber) of property changes.
                    // This is particularly necessary in order to get validation error messages and HasEditChanges state 
                    // up to top-level PMs where error summary information is presented.
                    _propertyChangedHandler = new PropertyChangedEventHandler(PersistableDataObject_PropertyChanged);
                    pdoOut.PropertyChanged += _propertyChangedHandler;

                    ChainValidationEvents(pdoOut);
                }

                // We're not using MarkChangedAndNotify() here since it internally updates IsDirty, 
                // which is not appropriate here.
                OnPropertyChanged(propertyExpression);

                return true;
            }

            return false;
        }


        /// <summary>
        /// Property changed event handler used to bubble child PDO notifications up to subscribed pdo.
        /// </summary>
        /// <param name="sender">child PDO</param>
        /// <param name="e">PropertyChangedEventArgs</param>
        void PersistableDataObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, e);
            }
        }


        /// <summary>
        /// Validate provided Guid to enusure it is not null or empty. 
        /// </summary>
        /// <param name="guid">Guid to validate</param>
        /// <returns>Original Guid, or a new one if needed.</returns>
        protected Guid ValidateGuid(Guid guid)
        {
            if (guid.IsNullOrEmpty())
            {
                guid = Guid.NewGuid();
            }
            return guid;
        }


        /// <summary>
        /// Set valueOut to valueTo if values different. 
        /// If the int value changes, the dirty flag is set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetInt(ref int valueOut, int valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetInt(ref int? valueOut, int? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Set valueOut to valueTo if values different. 
        /// If the int value changes, the dirty flag is set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetInt16(ref Int16 valueOut, Int16 valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }


        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetInt16(ref Int16? valueOut, Int16? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Set valueOut to valueTo if values different. 
        /// If the int value changes, the dirty flag is set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetInt64(ref Int64 valueOut, Int64 valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Set valueOut to valueTo if values different. 
        /// If the int value changes, the dirty flag is set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetLong(ref long valueOut, long valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Set valueOut to valueTo if values different. 
        /// If the int value changes, the dirty flag is set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetInt64(ref Int64? valueOut, Int64? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Set strOut to setTo if setTo. If the string is empty, set to null. 
        /// If the string changes, the dirty flag is set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetString(ref string valueOut, string valueTo, Expression<Func<object>> propertyExpression)
        {
            return internalSetString(ref valueOut, valueTo, int.MaxValue, propertyExpression);
        }

        /// <summary>
        /// Set strOut to setTo if setTo is less than or equal to maxLength. If the
        /// string is empty, set to null. If the string changes, the dirty flag is
        /// set to true.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetString(ref string valueOut, string valueTo, int maxLength, Expression<Func<object>> propertyExpression)
        {
            return internalSetString(ref valueOut, valueTo, maxLength, propertyExpression);
        }


        /// <summary>
        /// internal setstring method 
        /// </summary>
        private bool internalSetString(ref string valueOut, string valueTo, int maxLength, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            bool changed = false;
            if (valueTo != null)
            {
                valueTo = valueTo.Trim();

                if (valueTo.Length == 0)
                {
                    // CX valueTo = null;
                }
            }

            // NOTE: Don't check for max length here since this method is often called by 
            // the WPF binding framework which by default swallows exceptions.

            // DEPRECATED:
            //    throw new ArgumentOutOfRangeException(
            //        "setTo",
            //        string.Format(System.Globalization.CultureInfo.InvariantCulture,
            //        "The string property cannot hold text as large as what was specified. The maximum length is {0}.", maxLength));


            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                changed = true;
                MarkChangedAndNotify(propertyExpression);
            }

            return changed;
        }


        /// <summary>
        /// Set dateOut to setTo if they are different. The dirty flag is set to
        /// true if changed.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetDateTime(ref DateTime valueOut, DateTime valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }


        /// <summary>
        /// Set the Nullable dateOut to setTo if they are different. The dirty flag is set to
        /// true if changed.
        /// </summary>
        /// <returns>true if setting to a new value; false otherwise.</returns>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetDateTime(ref DateTime? valueOut, DateTime? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetBool(ref System.Boolean valueOut, System.Boolean valueTo, Expression<Func<object>> propertyExpression)
        {
            return SetBool(ref valueOut, valueTo, propertyExpression, ChangeTracking.On);
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetBool(ref System.Boolean valueOut, System.Boolean valueTo, Expression<Func<object>> propertyExpression, ChangeTracking trackChanges)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (trackChanges == ChangeTracking.On && ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;

        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetBool(ref System.Boolean? valueOut, System.Boolean? valueTo, Expression<Func<object>> propertyExpression)
        {
            return SetBool(ref valueOut, valueTo, propertyExpression, ChangeTracking.On);
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetBool(ref System.Boolean? valueOut, System.Boolean? valueTo, Expression<Func<object>> propertyExpression, ChangeTracking trackChanges)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (trackChanges == ChangeTracking.On && ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;

        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetFloat(ref float valueOut, float valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetDouble(ref double valueOut, double valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetDouble(ref double? valueOut, double? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetDecimal(ref decimal valueOut, decimal valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetDecimal(ref decimal? valueOut, decimal? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetGuid(ref Guid valueOut, Guid valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetGuid(ref Guid? valueOut, Guid? valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (valueOut != valueTo)
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetEntity<T>(ref T valueOut, T valueTo, Expression<Func<object>> propertyExpression)
        {
            return SetEntity<T>(ref valueOut, valueTo, propertyExpression, ChangeTracking.On);
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetEntity<T>(ref T valueOut, T valueTo, Expression<Func<object>> propertyExpression, ChangeTracking trackChanges)
        {
            CheckReadOnly();

            if ((valueOut == null && valueTo != null) || (valueOut != null && !valueOut.Equals(valueTo)))
            {
                if (trackChanges == ChangeTracking.On && ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }
            return false;
        }


        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetValue<T>(ref T valueOut, T valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly(); // checks internal read-only flags and throws exceptions if appropriate

            if ((valueOut == null && valueTo != null) || (valueOut != null && !valueOut.Equals(valueTo)))
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }
            return false;
        }


        /// <summary>
        /// Compare contents of two byte arrays.
        /// Candidate for optimization when dealing with large images.
        /// </summary>
        /// <param name="a1">byte array 1</param>
        /// <param name="a2">byte array 2</param>
        /// <returns>true if contents equal, false otherwise.</returns>
        private bool ByteArraysEqual(byte[] a1, byte[] a2)
        {
            if (a1 == null && a2 == null)
                return true;

            if (a1 == null || a2 == null)
                return false;

            if (a1.Length != a2.Length)
                return false;

            for (int i = 0; i < a1.Length; i++)
            {
                if (a1[i] != a2[i])
                {
                    return false;
                }
            }

            return true;
        }

        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "0#")]
        protected bool SetByteArray(ref byte[] valueOut,
                            byte[] valueTo, Expression<Func<object>> propertyExpression)
        {
            CheckReadOnly();

            if (!ByteArraysEqual(valueOut, valueTo))
            {
                if (ChangeTrackingEnabled)
                    MementoManager.Record(this, PDOMapper.GetNotificationPropertyName(propertyExpression), valueOut, valueTo);

                valueOut = valueTo;
                MarkChangedAndNotify(propertyExpression);
                return true;
            }

            return false;
        }

        #endregion

        #region ICloneable Implementation and Copy


        /// <summary>
        /// Get original, non-overridden hash code.
        /// </summary>
        /// <returns>hashcode</returns>
        public int GetHashCodeBase()
        {
            return base.GetHashCode();
        }


        /// <summary>
        /// Override GetHashCode to use primary key as hashcode.
        /// </summary>
        /// <returns>Hashcode based on primary key value.</returns>
        public override int GetHashCode()
        {
            if (null == PKeyID)
            {
                return base.GetHashCode();
            }
            else
            {
                if (IsNew)  //(Guid)PKeyID == Guid.Empty)
                {
                    return base.GetHashCode();
                }

                int hc = DBKey.GetHashCode();
                if (hc == 0)
                {
                    hc = base.GetHashCode();
                }

                return hc;
            }
        }

        public override bool Equals(object obj)
        {
            PersistableDataObject p = obj as PersistableDataObject;
            if (p == null)
            {
                return false;
            }
            else
            {
                // When we don't have a primary key, fall back to direct object comparison.
                if (PKeyID == null || p.PKeyID == null)
                {
                    return (this == obj);
                }

                // If new (no primary key yet), fall back to direct object comparison.
                if (IsNew)
                {
                    return (this == obj);
                }

                return this.DBKey.Equals(p.DBKey);
            }
        }


        /// <summary>
        /// Sever relationship to database by setting primary and foreign key value(s) to null.  
        /// </summary>
        /// <param name="pdo">PersistableDataObject to sever from database</param>
        internal void SeverPersistenceRelationships()
        {
            Type pdoType = this.GetType();

            // If this is an item in a list, null out list foreign key property
            if (null != this.ListContainer)
            {
                string fkeyName = this.ListContainer.ParentForeignKeyName;
                if (null != fkeyName) // only if there is a parent foreign key
                {
                    ReflectionHelper.SetObjectPropertyValue(this, fkeyName, null);
                }
            }

            // Walk inheritance chain, nulling out primary key properties
            while (pdoType != typeof(PersistableDataObject))
            {
                PropertyInfo pi = PDOMapper.GetPrimaryKeyPropertyInfo(pdoType);
                if (null != pi)
                {
                    pi.SetValue(this, null, null);
                }
                pdoType = pdoType.BaseType;
            }
        }


        /// <summary>
        /// Tracks PDO when walking object graph so we can detect and avoid circular references.
        /// </summary>
        /// <param name="pdo">PDO to check against call trace list</param>
        /// <param name="callTrace">list of PDOs comprising call trace</param>
        /// <returns>true if pdo is in call trace, false otherwise</returns>
        private static IPersistableData InCallTrace(IPersistableData pdo, Dictionary<int, IPersistableData> callTrace)
        {
            int hashCode = pdo.GetHashCode();

            if (callTrace.ContainsKey(hashCode))
            {
                return callTrace[hashCode];
            }

            callTrace.Add(hashCode, pdo);

            return null;
        }



        private static void UpdateCallTrace(IPersistableData pdo, Dictionary<int, IPersistableData> callTrace)
        {
            try
            {
                callTrace[pdo.GetHashCode()] = pdo;
            }
            catch (Exception ex)
            {
                int foo = 2;
            }
        }

        private static PersistableDataObject CoreClone(PersistableDataObject pdoSource, PersistableDataObject parent,
            bool twin, Dictionary<int, IPersistableData> callTrace)
        {
            // First we create an instance of this specific PersistableDataObject-based type.
            PersistableDataObject newObject = (PersistableDataObject)Activator.CreateInstance(pdoSource.GetType());
            newObject.ChangeTrackingEnabled = false;
            newObject.Parent = parent;

            // Get the array of fields for the new type instance.
            FieldInfo[] fields = GetHierarchyFields(newObject, false);

            // Use private fields again...
            foreach (FieldInfo fi in fields)
            {
                // Do not clone automatic property backing fields.
                // Treat them the same as if marked with NonPersisted.

                // NOTE: While it is *possible* to clone the automatic property backing fields, we don't want
                // to since we rely on explicitly delcared backing fields in other areas of the framework.
                // At some point, we may want to move away from working with fields to working with properties
                // in these scenarios, at which time we should re-evaluate this logic.
                if (fi.Name.EndsWith("__BackingField"))
                    continue;

                if (typeof(IPersistableData).IsAssignableFrom(fi.FieldType))
                {
                    IPersistableData pdo = fi.GetValue(pdoSource) as IPersistableData;
                    if (null != pdo)
                    {
                        IPersistableData clonedPdo = InCallTrace(pdo, callTrace);
                        if (null == clonedPdo)
                        {
                            IPersistableData newPdo = null;
                            if (twin)
                            {
                                // Skip this field if it is marked with NonPersisted attribute.
                                object[] npa = fi.GetCustomAttributes(typeof(NonPersistedAttribute), false);

                                if (npa.Length == 0)
                                {
                                    newObject.SeverPersistenceRelationships();
                                    newPdo = pdo.Twin(newObject) as IPersistableData;
                                }
                            }
                            else
                            {
                                newPdo = pdo.Clone(callTrace) as IPersistableData;
                            }

                            fi.SetValue(newObject, newPdo);
                            UpdateCallTrace(newPdo, callTrace);
                        }
                        else
                        {
                            // Since we found this pdo in the call trace, we've already cloned it 
                            fi.SetValue(newObject, clonedPdo);
                        }
                    }
                }
                else // non PDO objects
                {
                    Type ICloneType = fi.FieldType.GetInterface("ICloneable", true);
                    ICloneable IClone = null;
                    if (ICloneType != null)
                    {
                        IClone = (ICloneable)fi.GetValue(pdoSource);
                    }

                    // If the field is marked as NonPersisted - do not twin it.
                    object[] nta = new object[0];
                    if (twin)
                    {
                        nta = fi.GetCustomAttributes(typeof(NonPersistedAttribute), false);
                    }

                    if (nta.Length == 0)
                    {
                        if (null != IClone)
                        {
                            // If we found a clone method, use it.
                            fi.SetValue(newObject, IClone.Clone());
                        }
                        else
                        {
                            // If the field doesn't support the ICloneable interface then just set it.
                            fi.SetValue(newObject, fi.GetValue(pdoSource));
                        }


                        // Now we check if the object supports the IEnumerable interface.  If it does
                        // we need to enumerate all its items and check if they support the ICloneable interface.
                        Type IEnumerableType = fi.FieldType.GetInterface("IEnumerable", true);
                        if (IEnumerableType != null)
                        {
                            // Check for IEnumerable interface...
                            IEnumerable IEnum = (IEnumerable)fi.GetValue(pdoSource);

                            if (null != IEnum)
                            {
                                // Use IList or IDictionary interfaces to iterate on collections.
                                Type IListType = fi.FieldType.GetInterface("IList", true);
                                Type IDicType = fi.FieldType.GetInterface("IDictionary", true);

                                int j = 0;
                                if (IListType != null)
                                {
                                    // Copy elements into separate list to guard against changes to the enumeration 
                                    // as a side-effect of other clone/copy operations.
                                    List<object> source = new List<object>();
                                    foreach (object obj in IEnum)
                                    {
                                        source.Add(obj);
                                    }
                                    IList list = (IList)fi.GetValue(newObject);

                                    foreach (object obj in source)
                                    {
                                        ICloneType = obj.GetType().GetInterface("ICloneable", true);

                                        if (ICloneType != null)
                                        {
                                            // If it does support the ICloneable interface, use it.
                                            ICloneable clone = (ICloneable)obj;
                                            list[j] = clone.Clone();
                                        }

                                        // NOTE: If the item in the list does not support ICloneable then
                                        // the cloned list item will be the same item as in the original list
                                        // (if this type is a reference type).

                                        j++;
                                    }
                                }
                                else if (IDicType != null)
                                {
                                    // Check for Dictionary interface.
                                    IDictionary dic = (IDictionary)fi.GetValue(newObject);

                                    j = 0;
                                    int count = (IEnum as IDictionary).Count;
                                    List<object> tempKeys = new List<object>();
                                    List<object> tempValues = new List<object>();
                                    foreach (DictionaryEntry de in (IEnum as IDictionary))
                                    {
                                        tempKeys.Add(de.Key);
                                        tempValues.Add(de.Value);
                                    }

                                    for (int k = 0; k < count; k++)
                                    {
                                        ICloneType = tempValues[k].GetType().GetInterface("ICloneable", true);
                                        if (ICloneType != null)
                                        {
                                            ICloneable clone = (ICloneable)tempValues[k];
                                            dic[tempKeys[k]] = clone.Clone();
                                        }
                                        j++;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            return newObject;
        }



        private static bool IsPersistableDataObject(PropertyInfo pi)
        {
            return typeof(PersistableDataObject).IsAssignableFrom(pi.PropertyType);
        }

        private static bool IsPersistableDataObjectList(PropertyInfo pi)
        {
            bool results = false;
            if (pi.PropertyType.IsGenericType)
            {
                Type[] args = pi.PropertyType.GetGenericArguments();

                if (args.Length == 1 &&
                    typeof(PersistableDataObject).IsAssignableFrom(args[0]))
                {
                    results = true;
                }
            }
            return results;
        }


        /// <summary>
        /// Clone this object. Note that properties marked with XmlIgnore, or backing fields 
        /// marked with NonPersisted are not cloned.
        /// </summary>
        /// <returns>Reference to the new cloned object.</returns>
        public object Clone()
        {
            Dictionary<int, IPersistableData> callTrace = new Dictionary<int, IPersistableData>();
            return Clone(callTrace);
        }

        public IPersistableData Clone(Dictionary<int, IPersistableData> callTrace)
        {
            using (TimedLock.Lock(SyncRoot))
            {
                // Copy dirty and new flags independently, since creation and setting other properties modifies these values.
                bool isDirty = this.IsDirty;
                bool isNew = this.IsNew;

                PersistableDataObject newPDO = CoreClone(this, Parent, false, callTrace);

                newPDO.IsDirty = isDirty;
                newPDO.IsNew = isNew;

                newPDO.ChangeTrackingEnabled = true;

                return newPDO;
            }
        }


        /// <summary>
        /// Create a twin of this object; i.e. a clone that can be modified and saved without affecting original.
        /// 
        /// Twin operates by first doing a deep clone of the PersistableDataObject, and then
        /// severing all foreign key relationships.  Thus, new database entries are created 
        /// for the root object and child objects when Save recurses down the object graph.
        ///
        /// In order to create new copies of child objects, those child objects first have to
        /// be present on the parent.  To ensure you get copies of all the child objects you 
        /// want, check them in an override of this method. The simple act of checking for 
        /// their presence causes a lazy load, thus ensuring new copies are created in the database.
        /// 
        /// Property backing-fields marked with [NonPersisted] attribute are not Twinned even if 
        /// loaded.
        /// </summary>
        /// <returns>Reference to the new twin object.</returns>
        public virtual IPersistableData Twin(PersistableDataObject parent)
        {
            using (TimedLock.Lock(SyncRoot))
            {
                bool isDirty = this.IsDirty;

                Dictionary<int, IPersistableData> callTrace = new Dictionary<int, IPersistableData>();
                PersistableDataObject newPDO = CoreClone(this, parent, true, callTrace);
                newPDO.SeverPersistenceRelationships();

                if (isDirty)
                {
                    // Don't clear dirty flag if it's already true.  
                    // Adding items to a PersistableDataObjectList child set the parent as dirty - don't unset that.
                    newPDO.IsDirty = isDirty;
                }

                newPDO.IsNew = true;

                newPDO.ChangeTrackingEnabled = true;

                return newPDO;
            }
        }


        /// <summary>
        /// Copy all field (non-public, instance) values from the source object. Non-recursive.
        /// </summary>
        /// <param name="source">source object to copy from</param>
        public void CopyFromExtensionBase(object source)
        {

            //CX TODO Copy all fields from source to corresponding fields on this (=target).
            //        Should include fields from base classes.  

            Debug.Assert(source.GetType().IsAssignableFrom(this.GetType()));

            // Get all the fields in this class
            FieldInfo[] sourceField = source.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
            FieldInfo[] targetField = this.GetType().BaseType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

            Dictionary<string, FieldInfo> dic = new Dictionary<string, FieldInfo>();
            foreach (FieldInfo fi in targetField)
            {
                dic.Add(fi.Name, fi);
            }

            // Copy each source value over to correspondingly named target field
            foreach (FieldInfo sfi in sourceField)
            {
                if (dic.ContainsKey(sfi.Name))
                {
                    FieldInfo tfi = dic[sfi.Name];
                    tfi.SetValue(this, sfi.GetValue(source));
                }
            }
        }


        /// <summary>
        /// Copy all database defined property values, but not any other public or private fields,
        /// from the source object to this.
        /// </summary>
        /// <param name="source">source object to copy from</param>
        public void CopyPersistedValues(PersistableDataObject source)
        {
            // TODO: Replace with one of the other existing Copyxxx methods

            Debug.Assert(source.GetType().IsAssignableFrom(this.GetType()));

            // Set value of primary key.  
            // We use reflection in order to keep the commonly used PKeyID property read-only.
            ReflectionHelper.SetObjectPropertyValue(this, PrimaryKeyPropertyName, source.PKeyID);


            Type pdoType = source.GetType();

            // All database persisted properties are defined in the FieldNames struct in generated 
            // PersistableDataObject derived classes.
            Type ft = pdoType.GetNestedType("FieldNames");
            FieldInfo[] sourceField = ft.GetFields(BindingFlags.Public | BindingFlags.Static);

            // Copy each source value over to correspondingly named target field.
            foreach (FieldInfo sfi in sourceField)
            {
                object val = ReflectionHelper.GetObjectPropertyValue(source, sfi.Name);
                ReflectionHelper.SetObjectPropertyValue(this, sfi.Name, val);
            }
        }


        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        //        public event EventHandler<PDORoutedEventArgs> RoutedPropertyChanged;


        /// <summary>
        /// Raise PropertyChanged event for a specified property.
        /// </summary>
        /// <param name="propertyName"></param>
        public void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                // NOTE: Future enhancement: When propertyName is null or empty, send notifications for all properties.
                // But, how useful would that be?  We'd actually have to recurse down PDO child object 
                // and notify on those too if we want to ensure that all bindings received 
                // a property change notification.

                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public void OnPropertyChanged(Expression<Func<object>> expression)
        {
            OnPropertyChanged(PDOMapper.GetNotificationPropertyName(expression));
        }


        /// <summary>
        /// Set dirty state and call PropertyChanged event handler.
        /// </summary>
        /// <param name="propertyExpression">lambda expression identifying calling property setter.</param>
        /// </summary>
        public void MarkChangedAndNotify(Expression<Func<object>> propertyExpression)
        {
            IsDirty = true;

            if (PropertyChanged != null)
            {
                string lambdaPropertyName = PDOMapper.GetNotificationPropertyName(propertyExpression);

                OnPropertyChanged(lambdaPropertyName);
            }
        }


        #endregion

        #region List sort support

        virtual public void Sort(IPersistableDataObjectList list)
        {
            // no-op in base class - overridden in generated concrete classes
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // free managed resources
                if (Parent != null)
                {
                    Parent = null;
                    _listContainer = null;
                }
            }
        }

        #endregion

        #region IDataErrorInfo Members and Supporting code

        void SetValidationResult(MemberInfo mi, string validationResult)
        {
            if (null == _validationErrors)
            {
                _validationErrors = new Dictionary<MemberInfo, string>();
            }

            bool changed = false;
            if (_validationErrors.ContainsKey(mi))
            {
                if (_validationErrors[mi] != validationResult)
                {
                    changed = true;
                    _validationErrors[mi] = validationResult;
                }
            }
            else
            {
                changed = true;
                _validationErrors.Add(mi, validationResult);
            }


            if (changed)
            {
                _errorSummary = null;
                OnPropertyChanged(() => this.ErrorSummary);
            }

        }





        /// <summary>
        /// Collect all validation error information, including rules that are *not* associated with
        /// a single particular property.
        /// </summary>
        /// <param name="sb">StringBuilder into which validation error information is added.</param>
        private void BuildValidationErrors(StringBuilder sb, PersistableDataObject rootPdo)
        {
            PropertyInfo[] piArray = this.GetType().GetProperties(); // TODO: cache for optimization
            foreach (PropertyInfo pi in piArray)
            {
                ValidationRuleAttribute vra = ReflectionHelper.GetAttribute<ValidationRuleAttribute>(pi);
                if (null != vra)
                {
                    string error = ReflectionHelper.GetObjectPropertyValue(this, pi) as string;
                    string sourceTypeName = string.Empty;
                    if (this != rootPdo)
                    {
                        sourceTypeName = " [" + this.GetType().Name + "]";
                    }

                    if (!string.IsNullOrEmpty(error))
                    {
                        error = string.Format("{0} : {1}{2}", vra.PropertyName, error, sourceTypeName);
                        sb.Append(error + Environment.NewLine);
                    }
                    SetValidationResult(pi, error);
                }
            }
        }

        /// <summary>
        /// Get validation error for single, provided named property.  
        /// Note that it is possible to have multiple validation rules for a property name on a single Type.
        /// </summary>
        /// <param name="propertyName">property name (not null or empty)</param>
        /// <returns></returns>
        private string GetPropertyValidationErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                string msg = LocalizationHelper.ErrorDefMsg("PDO_PROPNAMENULL", "null or empty property name");
                ErrorDispatcher.Instance.NotifyError(new ErrorNotifyEventArgs() { Message = msg });
            }


            StringBuilder sb = new StringBuilder();
            PropertyInfo[] piArray = this.GetType().GetProperties(); // TODO: cache for optimization
            foreach (PropertyInfo pi in piArray)
            {
                ValidationRuleAttribute vra = ReflectionHelper.GetAttribute<ValidationRuleAttribute>(pi);
                if (null != vra && vra.PropertyName == propertyName)
                {
                    // NOTE: There may be multiple validation rules defined for a single property.

                    string error = ReflectionHelper.GetObjectPropertyValue(this, pi) as string;
                    if (!string.IsNullOrEmpty(error))
                    {
                        error = vra.Description + ": " + error;
                        sb.Append(error + Environment.NewLine);
                    }
                    SetValidationResult(pi, error);
                }
            }

            return sb.ToString();
        }


        /// <summary>
        /// Checks Validation rule attributes to determine if provided property is required.
        /// </summary>
        /// <param name="propertyName">property name</param>
        /// <returns>true if required, false otherwise</returns>
        public bool IsRequiredProperty(string propertyName)
        {
            bool result = false;

            if (string.IsNullOrEmpty(propertyName))
            {
                string msg = LocalizationHelper.ErrorDefMsg("PDO_PROPNAMENULL", "null or empty property name");
                ErrorDispatcher.Instance.NotifyError(new ErrorNotifyEventArgs() { Message = msg });
            }


            PropertyInfo[] piArray = this.GetType().GetProperties(); // TODO: cache for optimization
            foreach (PropertyInfo pi in piArray)
            {
                RequiredFieldAttribute rfa = ReflectionHelper.GetAttribute<RequiredFieldAttribute>(pi);
                if (null != rfa && rfa.PropertyName == propertyName)
                {
                    // Find matching field requirement rule(s) and get its value.  

                    bool? required = ReflectionHelper.GetObjectPropertyValue(this, pi) as bool?;
                    if (required.HasValue)
                    {
                        // In the case of multiple field requirement rules, any one rule can make the field required.
                        result |= required.Value;
                    }

                    break;
                }
            }

            return result;
        }

        /// <summary>
        /// Return concatenated string of all validation error messages for this PersistableDataObject 
        /// and all child PersistableDataObjects, separated by Environment.NewLine.
        /// </summary>
        private void CollectValidationErrors(StringBuilder sb, Dictionary<int, IPersistableData> callTrace, PersistableDataObject rootPdo)
        {
            if (null != InCallTrace(this, callTrace))
                return;


            using (TimedLock.Lock(SyncRoot))
            {
                // Turn off change tracking while we're doing validation.
                bool oldChangeTracking = this.ChangeTrackingEnabled;
                this.ChangeTrackingEnabled = false;

                // Set audit info prior to validation checks to ensure any required audit fields are populated.
                SetAuditInfo();

                // Build-up any error messages on this object.
                this.BuildValidationErrors(sb, rootPdo);

                this.ChangeTrackingEnabled = oldChangeTracking;
            }

            // Next, recurse into any PDO child properties.
            // NOTE: We're not checking for any new or dirty state - may want to add that kind of optimization sometime.

            // Use private fields instead of public properties, since accessing the property
            // for a lazy-loaded child object will cause a database load or create an empty object.
            FieldInfo[] fields = GetHierarchyFields(this, true);
            foreach (FieldInfo fi in fields)
            {
                object obj = fi.GetValue(this);
                if (obj != null)
                {
                    if (typeof(PersistableDataObject).IsAssignableFrom(obj.GetType()))
                    {
                        PersistableDataObject pdo = obj as PersistableDataObject;
                        pdo.CollectValidationErrors(sb, callTrace, rootPdo);

                        SetValidationResult(fi, pdo.ErrorSummary);
                    }
                }
            }


            // Check for any children PersistableDataObject lists
            foreach (FieldInfo fi in fields)
            {
                object obj = fi.GetValue(this);
                if (obj != null)
                {
                    Type oType = obj.GetType();
                    if (oType.IsGenericType)
                    {
                        Type[] args = oType.GetGenericArguments();

                        if (args.Length == 1 &&
                            args[0].BaseType == typeof(PersistableDataObject))
                        {
                            IPersistableDataObjectList list = obj as IPersistableDataObjectList;
                            if (null != list)
                            {
                                foreach (PersistableDataObject pdo in list)
                                {
                                    pdo.CollectValidationErrors(sb, callTrace, rootPdo);

                                    SetValidationResult(fi, pdo.ErrorSummary);
                                }
                            }
                        }
                    }
                }

            }
        }

        public void UpdateValidationErrors()
        {
            OnPropertyChanged(() => ((IDataErrorInfo)this).Error);
            OnPropertyChanged(() => this.ErrorSummary);
        }

        /// <summary>
        /// Returns property name on provided parent object that gives value of provided pdo.
        /// Note that this only works for PersistableDataObjects decorated with the Linq Association attribute.
        /// </summary>
        /// <param name="parent">Parent PersistableDataObject</param>
        /// <param name="pdo">PersistableDataObject whose parent property name we want.</param>
        /// <returns>property name or null if not found</returns>
        private static string GetParentReferencePropertyName(PersistableDataObject parent, PersistableDataObject pdo)
        {
            string parentPropertyName = null;

            object[] propertyInfo = parent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
            foreach (PropertyInfo pi in propertyInfo)
            {
                object[] attrs = pi.GetCustomAttributes(typeof(AssociationAttribute), false);
                foreach (AssociationAttribute a in attrs)
                {
                    string fieldName = a.Storage;
                    object o = ReflectionHelper.GetObjectFieldValue(parent, fieldName);

                    IPersistableDataObjectList list = o as IPersistableDataObjectList;
                    if (null != list)
                    {
                        foreach (PersistableDataObject pli in list)
                        {
                            if (pdo.Equals(pli))
                            {
                                parentPropertyName = pi.Name;
                                int index = list.IndexOf(pli);
                                parentPropertyName += ("[" + index + "]");
                                return parentPropertyName;
                            }
                        }
                    }
                    else
                    {
                        if (pdo.Equals(o))
                        {
                            parentPropertyName = pi.Name;
                            return parentPropertyName;
                        }
                    }
                }
            }
            return parentPropertyName;
        }



        private string _rootParentPropertyPath;

        /// <summary>
        /// Property path from root parent object to this PersistableDataObject. 
        /// Note that this only works for PersistableDataObjects decorated with the Linq Association attribute.
        /// </summary>
        [XmlIgnore]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public string RootParentPropertyPath
        {
            get
            {
                if (null == _rootParentPropertyPath)
                {
                    _rootParentPropertyPath = GetPropertyPath(this, string.Empty);
                }
                return _rootParentPropertyPath;
            }
        }


        /// <summary>
        /// Returns the path to provided leafPDO object from the root parent object.
        /// Note that this only works for PersistableDataObjects decorated with the Linq Association attribute.
        /// </summary>
        /// <param name="leafPDO">PDO object of interest</param>
        /// <param name="propertyName">name of property (optional)</param>
        /// <returns>Path from root parent to leafPDO, with appended propertyName.</returns>
        public static string GetPropertyPath(PersistableDataObject leafPDO, string propertyName)
        {
            PersistableDataObject pdo = leafPDO;

            StringBuilder pathBuilder = new StringBuilder();
            while (null != pdo.Parent)
            {
                string leafName = GetParentReferencePropertyName(pdo.Parent, pdo);
                if (leafName == null)
                {
                    Debug.WriteLine("PDO : parent-child relationship not found."); // CX 
                }

                leafName += ".";
                pathBuilder.Insert(0, leafName);
                pdo = pdo.Parent;
            }

            if (string.IsNullOrEmpty(propertyName))
            {
                // Remove trailing "."
                if (pathBuilder.Length > 0)
                {
                    pathBuilder.Remove(pathBuilder.Length - 1, 1);
                }
            }
            else
            {
                // Append provided property name
                pathBuilder.Append(propertyName);
            }

            return pathBuilder.ToString();
        }



        [XmlIgnore]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public bool IsSelfReference
        {
            get { FindHierarchyPosition(); return _isSelfReference; }
            private set { _isSelfReference = value; }
        }


        private void FindHierarchyPosition()
        {
            if (_nestingLevel == -1)
            {
                _nestingLevel = 0;
                _rootParent = this;
                _isSelfReference = false;
                while (null != _rootParent.Parent)
                {
                    _nestingLevel++;
                    _rootParent = _rootParent.Parent;

                    if (SourceDatabaseTableName == _rootParent.SourceDatabaseTableName)
                    {
                        if (_nestingLevel > 1)
                        {
                            _isSelfReference = true;
                        }
                    }

                    if (_rootParent.Parent == null && !(_rootParent is PersistableDataObjectRoot))
                    {
                        // There could be advantages to having a known, fixed type as the root of all 
                        // object graphs, but as of now, there is no need to enforce this.

                        int foo = 2; // CX
                    }
                }
            }
        }

        [XmlIgnore]
        public int NestingLevel
        {
            get { FindHierarchyPosition(); return _nestingLevel; }
        }

        /// <summary>
        /// Get root (top-level) parent.
        /// </summary>
        [XmlIgnore]
        public PersistableDataObject RootParent
        {
            get
            {
                FindHierarchyPosition(); return _rootParent;
            }
        }


        /// <summary>
        /// Check valiadtion for provided property name.
        /// Implementation of IDataErrorInfo.this[] indexer.
        /// </summary>
        /// <param name="propertyName">Name of property to check validation on, or null for all.</param>
        /// <returns>validation error message, or empty string if none.</returns>
        [XmlIgnore]
        string IDataErrorInfo.this[string propertyName]
        {
            get
            {
                string result = string.Empty;
                if (null == propertyName)
                {
                    StringBuilder sb = new StringBuilder();
                    Dictionary<int, IPersistableData> callTrace = new Dictionary<int, IPersistableData>();
                    CollectValidationErrors(sb, callTrace, this);
                    result = sb.ToString().TrimEnd();
                }
                else
                {
                    result = GetPropertyValidationErrors(propertyName).TrimEnd();

                    UpdateValidationErrors();
                    RootParent.UpdateValidationErrors(); // notify top-level parent of updated validation error info.

                    if (null != RootParent.ValidationChanged)
                    {
                        RootParent.ValidationChanged(this, new ValidationEventArgs(result));
                    }
                }

                return result;
            }
        }

        /// <summary>
        /// Combined summary of all validation error messages.
        /// </summary>
        [XmlIgnore]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public virtual string ErrorSummary
        {
            get
            {
                if (null == _errorSummary)
                {
                    if (_validationErrors != null)
                    {
                        foreach (string s in _validationErrors.Values)
                        {
                            if (!string.IsNullOrEmpty(s))
                            {
                                if (null != _errorSummary)
                                {
                                    _errorSummary += Environment.NewLine;
                                }
                                _errorSummary += s;
                            }
                        }
                    }
                }

                return _errorSummary;
            }
        }


        /// <summary>
        /// Combined error messages from all property validations.
        /// Implementation of IDataErrorInfo.Error property.
        /// </summary>
        [XmlIgnore]
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        string IDataErrorInfo.Error
        {
            get { return (this as IDataErrorInfo)[null]; }
        }


        /// <summary>
        /// Checks Break the glass rule attributes to determine if provided property requires
        /// authorization to be changed
        /// </summary>
        /// <param name="propertyName">property name</param>
        /// <returns>true if authorization is necessary, false otherwise</returns>
        

        #endregion


        #region IEditableObject Implementation

        Memento _memento;

        /// <summary>
        /// Create an internal copy of object and child object states.  
        /// Use with EndEdit, CancelEdit to restore to this internal state as necessary.
        /// </summary>
        public virtual void BeginEdit()
        {
            // Check for inappropriate call sequence
            if (null != _memento)
            {
                throw new System.InvalidOperationException(LocalizationHelper.ErrorDefMsg("PDO_BEGINEDITNEST",
                    "There is already a BeginEdit active on this object. The next call should be a CancelEdit or EndEdit"));
            }
            else
            {
                _memento = MementoManager.CreateMemento(this);
            }
        }


        /// <summary>
        /// True if this object, or any child object, has been changed since the last call to BeginEdit.
        /// </summary>
        [XmlIgnore]
        public bool HasEditChanges
        {
            get
            {
                if (null == _memento)
                    return false;

                return _memento.HasChanges;
            }
        }

        /// <summary>
        /// Restore object and child object states to condition saved by BeginEdit().
        /// Use with BeginEdit, EndEdit to manage this internal state as necessary.
        /// </summary>
        public virtual void CancelEdit()
        {
            if (null == _memento)
                return;

            MementoManager.Undo();
            MementoManager.ReleaseMemento(_memento);
            _memento = null;
        }

        /// <summary>
        /// Restore object and child object states to condition saved by BeginEdit().
        /// Use with BeginEdit, CancelEdit to manage this internal state as necessary.
        /// </summary>
        public virtual void EndEdit()
        {
            MementoManager.ReleaseMemento(_memento);
            _memento = null;
        }

        #endregion IEditableObject Implementation

    }
    public class ValidationEventArgs : EventArgs
    {
        private ValidationEventArgs() { }

        public ValidationEventArgs(string errorSummary)
        {
            ErrorSummary = errorSummary;
        }

        public string ErrorSummary { get; private set; }
    }

    public class ValidationInfo : MemberInfo
    {
        private string _name;

        public ValidationInfo(string name)
            : base()
        {
            _name = name;
        }
        public override string Name
        {
            get { return _name; }
        }

        public override Type DeclaringType
        {
            get { throw new NotImplementedException(); }
        }

        public override object[] GetCustomAttributes(Type attributeType, bool inherit)
        {
            throw new NotImplementedException();
        }

        public override object[] GetCustomAttributes(bool inherit)
        {
            throw new NotImplementedException();
        }

        public override bool IsDefined(Type attributeType, bool inherit)
        {
            throw new NotImplementedException();
        }

        public override MemberTypes MemberType
        {
            get { throw new NotImplementedException(); }
        }

        public override Type ReflectedType
        {
            get { throw new NotImplementedException(); }
        }
    }

}

